<?php
/**
 * quick_curd
 * ============================================================================
 * 版权所有 2024-2099 www.wltx666.com，并保留所有权利。
 * 网站地址: https://www.wltx666.com
 * ============================================================================
 * Author: 草上飞
 * Email: wei@alipay16.cn
 * QQ: 2719875001
 * Date: 2025/11/11
 * 描述:
 */

namespace app\service;

use think\Exception;
use think\facade\Db;

class UpdateService
{
    private $db;
    private $table_upgrade_log = 'web_upgrade_log';

    public function __construct()
    {
        $this->db = Db::connect();
        $dbConfig = config('database');
        $dbConfig = $dbConfig['connections'][$dbConfig['default']];
        $prefix = $dbConfig['prefix'];
        $this->table_upgrade_log = $prefix . 'web_upgrade_log';
    }

    //获取当前版本
    public function get_current_version()
    {
        // 获取当前版本信息
        $versionFile = app()->getRootPath() . '/update/version.json';
        if (file_exists($versionFile)) {
            $versionInfo = json_decode(file_get_contents($versionFile), true);
            $version = $versionInfo['version'] ?? '未知版本';
            $updateTime = date("Y-m-d H:i:s", filemtime($versionFile));
        } else {
            $version = '未找到版本信息';
            $updateTime = '';

            $sql = "SHOW TABLES LIKE 'web_upgrade_log'";
            $result = $this->db->query($sql);
            if ($result) {
                $sql = "SELECT * FROM `web_upgrade_log` ORDER BY `id` DESC LIMIT 1";
                $result = $this->db->query($sql);
                if ($result) {
                    $version = $result['version'];
                    $updateTime = $result['created_at'];
                }
            }
        }
        return data_return_arr('获取成功', 0, [
            'version' => $version,
            'update_time' => $updateTime
        ]);
    }

    //获取更新历史记录
    public function get_update_history($args = [])
    {
        $page = isset($args['page']) ? intval($args['page']) : 1;
        $limit = isset($args['limit']) ? intval($args['limit']) : 10;
        $offset = ($page - 1) * $limit;

        $sql = "SHOW TABLES LIKE '".$this->table_upgrade_log."'";
        $result = $this->db->query($sql);
        if (!$result) {
            return data_return_arr('未曾在线更新', 40001);
        }

        // 查询总数
        $countSql = "SELECT COUNT(*) as total FROM `".$this->table_upgrade_log."`";

        $countRes = $this->db->query($countSql);
        if (empty($countRes[0]['total'])) {
            return data_return_arr('未曾在线更新', 40001);
        }
        $total = $countRes[0]['total'];

        // 查询数据
        $sql = "SELECT * FROM `".$this->table_upgrade_log."` ORDER BY `id` DESC LIMIT {$offset}, {$limit}";
        $list = $this->db->query($sql);

        return data_return_arr('获取成功', 0, [
            'total' => $total,
            'list' => $list,
            'page' => $page,
            'limit' => $limit
        ]);
    }


    private function delete_dir($dir)
    {
        if (!is_dir($dir)) {
            return;
        }
        $files = scandir($dir);
        foreach ($files as $file) {
            if ($file == '.' || $file == '..') {
                continue;
            }
            $filePath = $dir . '/' . $file;
            if (is_dir($filePath)) {
                $this->delete_dir($filePath);
            } else {
                unlink($filePath);
            }
        }
        rmdir($dir);
    }

    //上传app
    public function upload_app($args = [])
    {
        $file = $_FILES['file'];
        $name = $file['name'];
        $type = $file['type'];
        if ($type != 'application/x-zip-compressed' && $type != 'application/zip') {
            return data_return_arr('请上传zip文件', 40010);
        }
        try {
            $startTime = microtime(true);
            $tempUnzip = app()->getRuntimePath() . "/tmp/web_update";
            $wwwRoot = app()->getRootPath();
            //去掉最后一个斜杠
            $wwwRoot = rtrim($wwwRoot, "\\");
            $wwwRoot = rtrim($wwwRoot, "/");
            //去掉最后一个斜杠
            $wwwRoot = rtrim($wwwRoot, "\\");
            $wwwRoot = rtrim($wwwRoot, "/");

            $uploadZip = $file['tmp_name'];
            //先递归清理临时目录
            if (is_dir($tempUnzip)) {
                $this->delete_dir($tempUnzip);
            }

            if (!is_dir($tempUnzip)) {
                mkdir($tempUnzip, 0755, true);
            }

            if (!is_writable($wwwRoot)) {
                //改成www-data权限
                chmod($wwwRoot, 0755);
                if (!is_writable($wwwRoot)) {
                    return data_return_arr('无权操作目录' . $wwwRoot, 40011);
                }
            }


            $zip = new \ZipArchive();
            if ($zip->open($uploadZip) === true) {
                $zip->extractTo($tempUnzip);
                $zip->close();
            } else {
                return data_return_arr('无法打开上传的ZIP文件', 40012);
            }

            if (!file_exists($tempUnzip . '/update/version.json')) {
                return data_return_arr('缺少版本信息文件update/version.json', 40013);
            }
            $versionInfo = json_decode(file_get_contents($tempUnzip . '/update/version.json'), true);
            // 2. 扫描差异
            $changes = [
                'added' => [],
                'changed' => [],
                'deleted' => []
            ];

            // 定义需要排除的目录
            $excludeDirs = [
                'runtime',
                'backup',
                'vendor',
                'temp' // 当前更新文件的目录
            ];

            $iterator = new \RecursiveIteratorIterator(
                new \RecursiveDirectoryIterator($tempUnzip, \RecursiveDirectoryIterator::SKIP_DOTS),
                \RecursiveIteratorIterator::LEAVES_ONLY
            );


            foreach ($iterator as $file) {
                if (!$file->isFile()) continue;

                $relPath = str_replace($tempUnzip, '', $file->getPathname());

                // 检查是否在排除目录中
                $shouldExclude = false;
                foreach ($excludeDirs as $excludeDir) {
                    if (strpos($relPath, DIRECTORY_SEPARATOR . $excludeDir . DIRECTORY_SEPARATOR) !== false ||
                        strpos($relPath, $excludeDir . DIRECTORY_SEPARATOR) === 1) {
                        $shouldExclude = true;
                        break;
                    }
                }

                if ($shouldExclude) continue;

                $newFile = $tempUnzip . $relPath;
                $curFile = $wwwRoot . $relPath;
                if (!file_exists($curFile)) {
                    //判断是否存在非法文件，比如.sh脚本、webshell相关的
                    $extension = strtolower(pathinfo($newFile, PATHINFO_EXTENSION));
                    if (in_array($extension, ['sh', 'py', 'rb', 'pl', 'cgi', 'exe', 'bat', 'vbs'])) {
                        $this->delete_dir($tempUnzip);
                        return data_return_arr('暂不支持添加这种类型文件：' . $newFile, 40014);
                    }
                    $changes['added'][] = $relPath;
                } elseif (md5_file($newFile) !== md5_file($curFile)) {
                    $changes['changed'][] = $relPath;
                }
            }


            $html = '';

            // 显示新增的文件
            if (!empty($changes['added'])) {
                $html .= '<div class="file-section"><h2>新增文件</h2>';
                foreach ($changes['added'] as $path) {
                    $html .= '<p><span class="added-file">新增:</span> ' . htmlspecialchars($path) . '</p>';
                }
                $html .= '</div>';
            }

            // 显示删除的文件
            if (!empty($changes['deleted'])) {
                $html .= '<div class="file-section"><h2>删除文件</h2>';
                foreach ($changes['deleted'] as $path) {
                    $html .= '<p><span class="deleted-file">删除:</span> ' . htmlspecialchars($path) . '</p>';
                }
                $html .= '</div>';
            }

            // 显示修改的文件及其详细差异
            if (!empty($changes['changed'])) {
                $html .= '<div class="file-section"><h2>修改文件</h2>';
                foreach ($changes['changed'] as $path) {
                    $html .= '<h3 class="changed-file">修改: ' . htmlspecialchars($path) . '</h3>';
                    $curFile = $wwwRoot . $path;
                    $newFile = $tempUnzip . $path;

                    // 确保两个文件都存在再进行比较
                    if (file_exists($curFile) && file_exists($newFile)) {
                        $diff = self::render($curFile, $newFile);
                        $html .= self::format($diff);
                    } else {
                        $html .= '<div class="diff-container"><div class="diff-header">文件差异对比</div>';
                        $html .= '<div class="diff-content"><div style="padding: 20px; color: #f00;">';
                        $html .= '错误：无法比较文件，一个或两个文件不存在</div></div></div>';
                    }
                }
                $html .= '</div>';
            }
        } catch (\Exception $e) {
            $this->delete_dir($tempUnzip);
            return data_return_arr('服务器内部错误:' . $e->getMessage(), 500);
        }
        if (empty($changes['added']) && empty($changes['changed'])) {
            $this->delete_dir($tempUnzip);
            return data_return_arr('没有需要更新的文件', 40015);
        }

        return data_return_arr('success', 0, [
            'changes' => $changes,
            'tempUnzip' => $tempUnzip,
            'wwwRoot' => $wwwRoot,
            'html' => $html,
            'versionInfo' => $versionInfo
        ]);


    }

    // 取消更新
    public function cancel_update($args)
    {
        $tempUnzip = $args['tempUnzip'];
        if (is_dir($tempUnzip)) {
            $this->delete_dir($tempUnzip);
        }
        return data_return_arr('取消更新成功', 0);
    }

    // 开始更新
    public function start_update($args = [])
    {
        $changes = $args['changes'];
        $tempUnzip = $args['tempUnzip'];
        if (empty($changes)) {
            $this->delete_dir($tempUnzip);
            return data_return_arr('没有需要更新的文件');
        }

        $added = $changes['added'];
        $changed = $changes['changed'];
        if (empty($added) && empty($changed)) {
            return data_return_arr('没有需要更新的文件', 40015);
        }

        $startTime = microtime(true);
        $tempUnzip = app()->getRuntimePath() . "/tmp/web_update";
        $wwwRoot = app()->getRootPath();
        //去掉最后一个斜杠
        $wwwRoot = rtrim($wwwRoot, "\\");
        $wwwRoot = rtrim($wwwRoot, "/");
        $backupDir = app()->getRuntimePath() . '/tmp/web_backup';
        //去掉最后一个斜杠
        $wwwRoot = rtrim($wwwRoot, "\\");
        $wwwRoot = rtrim($wwwRoot, "/");

        $iterator = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($tempUnzip, \RecursiveDirectoryIterator::SKIP_DOTS),
            \RecursiveIteratorIterator::LEAVES_ONLY
        );
        $logFileName = 'app_update_log';
        bs_log('======================================================================', '开始更新', $logFileName);

        bs_log($tempUnzip, '临时目录', $logFileName);
        bs_log($wwwRoot, '网站目录', $logFileName);
        $newVersion = '1.0.0';
        if (file_exists($tempUnzip . '/update/version.json')) {
            $versionInfo = json_decode(file_get_contents($tempUnzip . '/update/version.json'), true);
            $newVersion = $versionInfo['version'];
            bs_log($versionInfo, '更新信息', $logFileName);
        }
        $backupDir = $backupDir . '/' . $newVersion;
        if (!file_exists($backupDir)) {
            mkdir($backupDir, 0755, true);
        }
        $description = '';

        if ($newVersion && file_exists($tempUnzip . '/update/' . $newVersion . '.sql.php')) {
            //先更新数据库
            $sqls = include_once($tempUnzip . '/update/' . $newVersion . '.sql.php');

            try {
                $this->db->query('SET FOREIGN_KEY_CHECKS=0');
                $this->db->query('SET NAMES utf8');


                foreach ($sqls as $sql) {
                    $sql = trim($sql);
                    if (empty($sql)) continue;
                    $res = $this->db->execute($sql);
                    if ($res) {
                        bs_log($sql, 'SQL执行成功', $logFileName);
                    } else {
                        bs_log($sql, 'SQL执行失败', $logFileName);
                    }
                }
            } catch (\Exception $e) {
                $this->db->rollback();
                bs_log($e->getMessage(), '更新数据库失败', $logFileName);
                $this->delete_dir($tempUnzip);
                return data_return_arr('更新数据库失败', 500);

            }
            $this->db->query('SET FOREIGN_KEY_CHECKS=1');
            $this->db->commit();
        }


        foreach ($iterator as $file) {
            if (!$file->isFile()) continue;
            $relPath = str_replace($tempUnzip, '', $file->getPathname());
            $newFile = $tempUnzip . $relPath;
            $curFile = $wwwRoot . $relPath;
            if (!file_exists($curFile)) {
                // 新增文件
                $dir = dirname($curFile);
                if (!file_exists($dir)) {
                    mkdir($dir, 0777, true);
                }
                $copy = copy($newFile, $curFile);
                bs_log($curFile, '新增文件', $logFileName);
                $description .= '新增文件:' . $curFile . "\n";

            } elseif (md5_file($newFile) !== md5_file($curFile)) {
                //先备份文件
                $backupFile = $backupDir . '/' . $relPath;
                bs_log($backupFile, '备份文件', $logFileName);
                //创建目录
                if (!file_exists(dirname($backupFile))) {
                    mkdir(dirname($backupFile), 0777, true);
                }
                //备份文件
                copy($curFile, $backupFile);
                // 修改文件
                copy($newFile, $curFile);
                bs_log($curFile, '修改文件', $logFileName);
                $description .= '修改文件:' . $curFile . "\n";
            }
        }
        $ip = request()->ip();


        //如果表不存在要先创建表
        $sql = "CREATE TABLE IF NOT EXISTS `".$this->table_upgrade_log."` (
              `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
              `version` varchar(20) DEFAULT '' COMMENT '当前版本号',
              `description` varchar(1000) DEFAULT '' COMMENT '更新说明',
              `has_sql` tinyint(1) unsigned DEFAULT '0' COMMENT '是否有sql数据库更新',
              `created_at` datetime DEFAULT NULL COMMENT '更新时间',
              `ip_address` varchar(20) DEFAULT '' COMMENT '操作IP',
              `operation_user_id` int(11) unsigned DEFAULT '0' ,
            PRIMARY KEY (`id`)
          ) ENGINE=InnoDB DEFAULT CHARSET=utf8;";
        $res = $this->db->execute($sql);
        $operation_user_id = $args['admin_id']; //登录用户编号

         //防止sql注入
        $description = str_replace("'", "\'", $description);
        $newVersion = str_replace("'", "\'", $newVersion);


        $insertSql = "INSERT INTO `".$this->table_upgrade_log."` (`version`, `description`, `has_sql`, `created_at`, `ip_address`, `operation_user_id`) VALUES "
            . " ('$newVersion', '" . $description . "', '" . (!empty($sqls) ? 1 : 0) . "',' " . date('Y-m-d H:i:s') . "', '" . $ip . "', '" . intval($operation_user_id) . "')";



        $res = $this->db->execute($insertSql);;
        if ($res) {
            bs_log($insertSql, '更新日志插入成功', $logFileName);
        } else {
            bs_log($insertSql, '更新日志插入失败', $logFileName);
        }

        //清理临时目录
        $this->delete_dir($tempUnzip);
        bs_log('耗时：' . (microtime(true) - $startTime) . '秒', '更新完成', $logFileName);

        return data_return_arr('更新成功', 0, [
            'new_version' => $newVersion,
            'description' => $description,
            'has_sql' => !empty($sqls) ? '执行了SQL:' . join("\n", $sqls) : '未涉及SQL更新',
        ]);
    }


    // 生成差异
    private static function render($fromFile, $toFile)
    {
        // 读取文件内容，保留空行但移除换行符
        $fromLines = file($fromFile, FILE_IGNORE_NEW_LINES);
        $toLines = file($toFile, FILE_IGNORE_NEW_LINES);

        // 使用更简单的逐行比较算法
        $diff = self::lineDiff($fromLines, $toLines);

        return $diff;
    }

    /**
     * 使用改进的差异算法计算差异
     * 使用更智能的匹配策略，避免中间插入导致后续所有行被标记为修改
     */
    private static function lineDiff($fromLines, $toLines)
    {
        $m = count($fromLines);
        $n = count($toLines);

        // 如果文件较小，使用完整的LCS算法
        if ($m <= 5000 && $n <= 5000) {
            return self::lineDiffLCS($fromLines, $toLines);
        }

        // 对于大文件，使用改进的滑动窗口算法
        return self::lineDiffSliding($fromLines, $toLines);
    }

    /**
     * 使用LCS算法计算差异
     */
    private static function lineDiffLCS($fromLines, $toLines)
    {
        $lcs = self::computeLCS($fromLines, $toLines);

        // 根据LCS生成差异
        $diff = [];
        $i = 0;
        $j = 0;
        $lcsIdx = 0;

        while ($i < count($fromLines) || $j < count($toLines)) {
            // 如果两个文件当前位置的行都匹配LCS中的当前行
            if ($lcsIdx < count($lcs) &&
                $i < count($fromLines) &&
                $j < count($toLines) &&
                $fromLines[$i] === $lcs[$lcsIdx] &&
                $toLines[$j] === $lcs[$lcsIdx]) {
                $diff[] = [' ', $fromLines[$i]];
                $i++;
                $j++;
                $lcsIdx++;
            } // 如果源文件当前位置的行不在LCS中（需要删除）
            else if ($i < count($fromLines) &&
                ($lcsIdx >= count($lcs) || $fromLines[$i] !== $lcs[$lcsIdx])) {
                $diff[] = ['-', $fromLines[$i]];
                $i++;
            } // 如果目标文件当前位置的行不在LCS中（需要添加）
            else if ($j < count($toLines) &&
                ($lcsIdx >= count($lcs) || $toLines[$j] !== $lcs[$lcsIdx])) {
                $diff[] = ['+', $toLines[$j]];
                $j++;
            } else {
                // 防止无限循环
                break;
            }
        }

        return $diff;
    }

    /**
     * 使用改进的滑动窗口算法计算差异（用于大文件）
     */
    private static function lineDiffSliding($fromLines, $toLines)
    {
        $diff = [];
        $i = 0;
        $j = 0;
        $windowSize = 200; // 增大窗口大小以提高准确性

        while ($i < count($fromLines) || $j < count($toLines)) {
            // 如果两个文件当前位置的行相同
            if ($i < count($fromLines) && $j < count($toLines) &&
                $fromLines[$i] === $toLines[$j]) {
                $diff[] = [' ', $fromLines[$i]];
                $i++;
                $j++;
                continue;
            }

            // 在目标文件中查找匹配（向前查找）
            $foundInTo = false;
            $bestMatchInTo = -1;
            $searchRange = min($windowSize, count($toLines) - $j);
            for ($k = $j + 1; $k < $j + $searchRange && $k < count($toLines); $k++) {
                if ($fromLines[$i] === $toLines[$k]) {
                    $bestMatchInTo = $k;
                    $foundInTo = true;
                    break;
                }
            }

            // 在源文件中查找匹配（向前查找）
            $foundInFrom = false;
            $bestMatchInFrom = -1;
            $searchRange = min($windowSize, count($fromLines) - $i);
            for ($k = $i + 1; $k < $i + $searchRange && $k < count($fromLines); $k++) {
                if ($toLines[$j] === $fromLines[$k]) {
                    $bestMatchInFrom = $k;
                    $foundInFrom = true;
                    break;
                }
            }

            // 选择最佳匹配
            if ($foundInTo && $foundInFrom) {
                // 选择距离更近的匹配
                if (($bestMatchInTo - $j) <= ($bestMatchInFrom - $i)) {
                    // 在目标文件中找到匹配，说明中间添加了行
                    for ($l = $j; $l < $bestMatchInTo; $l++) {
                        $diff[] = ['+', $toLines[$l]];
                    }
                    $diff[] = [' ', $fromLines[$i]];
                    $i++;
                    $j = $bestMatchInTo + 1;
                } else {
                    // 在源文件中找到匹配，说明中间删除了行
                    for ($l = $i; $l < $bestMatchInFrom; $l++) {
                        $diff[] = ['-', $fromLines[$l]];
                    }
                    $diff[] = [' ', $toLines[$j]];
                    $i = $bestMatchInFrom + 1;
                    $j++;
                }
            } else if ($foundInTo) {
                // 只在目标文件中找到匹配
                for ($l = $j; $l < $bestMatchInTo; $l++) {
                    $diff[] = ['+', $toLines[$l]];
                }
                $diff[] = [' ', $fromLines[$i]];
                $i++;
                $j = $bestMatchInTo + 1;
            } else if ($foundInFrom) {
                // 只在源文件中找到匹配
                for ($l = $i; $l < $bestMatchInFrom; $l++) {
                    $diff[] = ['-', $fromLines[$l]];
                }
                $diff[] = [' ', $toLines[$j]];
                $i = $bestMatchInFrom + 1;
                $j++;
            } else {
                // 都没找到匹配，认为是修改了这一行
                if ($i < count($fromLines)) {
                    $diff[] = ['-', $fromLines[$i]];
                    $i++;
                }
                if ($j < count($toLines)) {
                    $diff[] = ['+', $toLines[$j]];
                    $j++;
                }
            }
        }

        return $diff;
    }

    /**
     * 计算最长公共子序列（LCS）
     * 使用动态规划算法
     */
    private static function computeLCS($fromLines, $toLines)
    {
        $m = count($fromLines);
        $n = count($toLines);

        // 如果文件太大，使用简化算法避免内存溢出
        if ($m > 5000 || $n > 5000) {
            return self::computeLCSSimple($fromLines, $toLines);
        }

        // 动态规划表
        $dp = [];
        for ($i = 0; $i <= $m; $i++) {
            $dp[$i] = array_fill(0, $n + 1, 0);
        }

        // 填充DP表
        for ($i = 1; $i <= $m; $i++) {
            for ($j = 1; $j <= $n; $j++) {
                if ($fromLines[$i - 1] === $toLines[$j - 1]) {
                    $dp[$i][$j] = $dp[$i - 1][$j - 1] + 1;
                } else {
                    $dp[$i][$j] = max($dp[$i - 1][$j], $dp[$i][$j - 1]);
                }
            }
        }

        // 回溯构建LCS
        $lcs = [];
        $i = $m;
        $j = $n;
        while ($i > 0 && $j > 0) {
            if ($fromLines[$i - 1] === $toLines[$j - 1]) {
                array_unshift($lcs, $fromLines[$i - 1]);
                $i--;
                $j--;
            } else if ($dp[$i - 1][$j] > $dp[$i][$j - 1]) {
                $i--;
            } else {
                $j--;
            }
        }

        return $lcs;
    }

    /**
     * 简化版LCS算法，用于大文件
     * 使用滑动窗口匹配，避免内存溢出
     */
    private static function computeLCSSimple($fromLines, $toLines)
    {
        $lcs = [];
        $i = 0;
        $j = 0;
        $windowSize = 100; // 滑动窗口大小

        while ($i < count($fromLines) && $j < count($toLines)) {
            if ($fromLines[$i] === $toLines[$j]) {
                $lcs[] = $fromLines[$i];
                $i++;
                $j++;
            } else {
                // 在窗口内查找匹配
                $found = false;
                $searchRange = min($windowSize, count($toLines) - $j);
                for ($k = $j + 1; $k < $j + $searchRange && $k < count($toLines); $k++) {
                    if ($fromLines[$i] === $toLines[$k]) {
                        // 找到匹配，跳过中间的行
                        $j = $k;
                        $found = true;
                        break;
                    }
                }

                if (!$found) {
                    // 在源文件中查找
                    $searchRange = min($windowSize, count($fromLines) - $i);
                    for ($k = $i + 1; $k < $i + $searchRange && $k < count($fromLines); $k++) {
                        if ($toLines[$j] === $fromLines[$k]) {
                            $i = $k;
                            $found = true;
                            break;
                        }
                    }
                }

                if (!$found) {
                    // 都没找到，跳过当前行
                    $i++;
                    $j++;
                }
            }
        }

        return $lcs;
    }

    private static function format($diff, $context = 3)
    {
        // 检查是否有实际的变更（新增或删除的行）
        $hasChanges = false;
        foreach ($diff as $line) {
            if ($line[0] === '+' || $line[0] === '-') {
                $hasChanges = true;
                break;
            }
        }

        // 如果没有实际变更，显示提示信息
        if (!$hasChanges) {
            $output = '<div class="diff-container">';
            $output .= '<div class="diff-header">文件差异对比</div>';
            $output .= '<div class="diff-content">';
            $output .= '<div style="padding: 20px; text-align: center; color: #666; font-style: italic;">';
            $output .= '文件已被标记为修改，但内容无实际变更（可能是时间戳或权限变更）';
            $output .= '</div>';
            $output .= '</div>'; // .diff-content
            $output .= '</div>'; // .diff-container
            return $output;
        }

        // 生成唯一容器ID
        $containerId = 'diff-' . uniqid();

        $output = '<div class="diff-container" id="' . $containerId . '" data-container-id="' . $containerId . '">';
        $output .= '<div class="diff-header">';
        $output .= '<span>文件差异对比</span>';
        $output .= '<div class="diff-navigation">';
        $output .= '<button class="diff-nav-btn" data-nav-direction="prev" data-container-id="' . $containerId . '" title="上一个修改 (↑)">⬆ 上一个</button>';
        $output .= '<span class="diff-nav-info" id="' . $containerId . '-info">0 / 0</span>';
        $output .= '<button class="diff-nav-btn" data-nav-direction="next" data-container-id="' . $containerId . '" title="下一个修改 (↓)">⬇ 下一个</button>';
        $output .= '</div>';
        $output .= '</div>';
        $output .= '<div class="diff-content">';

        $oldLine = 0;
        $newLine = 0;
        $changeIndex = 0; // 修改点的索引

        foreach ($diff as $idx => $line) {
            $type = $line[0];
            $content = isset($line[1]) ? $line[1] : '';

            // 更新行号
            if ($type === ' ' || $type === '-') {
                $oldLine++;
            }
            if ($type === ' ' || $type === '+') {
                $newLine++;
            }

            // 判断是否是修改点（新增或删除的行）
            $isChange = ($type === '+' || $type === '-');
            $changeId = '';
            $dataAttr = '';

            if ($isChange) {
                // 检查是否是新的修改块的开始
                // 修改块是一组连续的修改行（+或-），当遇到未修改的行时，修改块结束
                $isNewChangeBlock = false;
                if ($idx === 0) {
                    // 第一行就是修改，这是新块的开始
                    $isNewChangeBlock = true;
                } else {
                    $prevType = $diff[$idx - 1][0];
                    // 如果前一行是未修改的行（' '），则这是新修改块的开始
                    if ($prevType === ' ') {
                        $isNewChangeBlock = true;
                    }
                }

                if ($isNewChangeBlock) {
                    $changeIndex++;
                }

                // 为修改块的第一行添加ID，其他行只添加data属性
                if ($isNewChangeBlock) {
                    $changeId = $containerId . '-change-' . $changeIndex;
                }
                $dataAttr = ' data-change-index="' . $changeIndex . '"';
            }

            // 根据类型设置CSS类和行号显示
            switch ($type) {
                case '+':
                    $cssClass = 'diff-line diff-line-add';
                    $lineNumbers = '<span class="diff-line-number"></span><span class="diff-line-number">' . $newLine . '</span>';
                    // 添加 "+" 符号前缀，就像 Git 一样
                    $content = '+ ' . $content;
                    break;
                case '-':
                    $cssClass = 'diff-line diff-line-del';
                    $lineNumbers = '<span class="diff-line-number">' . $oldLine . '</span><span class="diff-line-number"></span>';
                    // 添加 "-" 符号前缀，就像 Git 一样
                    $content = '- ' . $content;
                    break;
                default: // ' ' (空格表示无变化)
                    $cssClass = 'diff-line diff-line-nor';
                    $lineNumbers = '<span class="diff-line-number">' . $oldLine . '</span><span class="diff-line-number">' . $newLine . '</span>';
                    // 添加空格前缀以对齐
                    $content = '  ' . $content;
                    break;
            }

            $idAttr = $changeId ? ' id="' . $changeId . '"' : '';
            $output .= '<div class="' . $cssClass . '"' . $idAttr . $dataAttr . '>' . $lineNumbers . htmlspecialchars($content) . '</div>';
        }

        // 存储修改点总数
        $output .= '<script type="text/javascript">';
        $output .= 'if (typeof window.diffChangeCounts === "undefined") { window.diffChangeCounts = {}; }';
        $output .= 'window.diffChangeCounts["' . $containerId . '"] = ' . $changeIndex . ';';
        $output .= '</script>';

        $output .= '</div>'; // .diff-content
        $output .= '</div>'; // .diff-container

        return $output;
    }
}
